Code Review for AI Code
06 Agentic Engineering 强调:"AI 写的代码也是你的代码"。但 AI 写的代码和人类写的有结构性差异,传统 review 习惯会漏掉关键问题。本篇讲清楚 AI 代码的特点、必查清单、以及自动化工具的工程边界。
学前说明
很多团队 review AI 代码用的还是 review 同事代码的方式:看看风格、扫一眼逻辑、跑下测试,OK 合并。
这在 AI 代码上风险显著更高,原因有三:
- AI 不会因为不确定而停下——它会"自信地写错"
- AI 不会因为不熟而问你——它会编造 API、参数、行为
- AI 写得太快——你审查不过来就草草 LGTM
2025 年下半年到 2026 年,这造成了大量隐蔽 bug 进入生产。本篇是给资深工程师的"AI 代码 review 手册"。
学习目标
- 理解 AI 代码与人类代码的 7 个结构性差异
- 掌握 AI 代码的 12 类常见隐患模式
- 设计两阶段 review 流程(机械检查 + 人类深度审查)
- 评估 CodeRabbit / Greptile 等自动化 review 工具的边界
- 建立团队级 AI 代码 review 规范
与现有知识的衔接
- 06 Agentic Engineering 第 2.6 节:代码审查文化(前置)
- 05 Parallel Agents:并行 Agent 让 review 更难
- 04 Lethal Trifecta:review 安全相关代码的特别注意
第一章:AI 代码的 7 个结构性差异
不是说 AI 代码"质量差",而是它和人类代码性质不同。理解差异是 review 的前提。
1.1 一致性 vs 变化性
人类代码:风格高度一致。同一个工程师写的代码,命名、错误处理、缩进风格基本统一。
AI 代码:在同一个 PR 里可能混用多种风格。前 100 行用 Result<T, E>,后 100 行用 try/catch。原因:AI 没有"长期个人偏好",每次生成时倾向最近上下文里的模式。
Review 影响:
- 不能假设"前面看了几个函数,后面也是这样"
- 必须全文一致性检查
1.2 命名的"似是而非"
人类:命名经过深思熟虑,反映对业务的理解。
AI:命名"听起来对",但可能不准确。
// AI 写的(看起来很合理)
function processUserData(user: User) {
// 实际上只读不改,但名字是 process
}
function getUser(id: string) {
// 实际上有副作用:缓存了结果
}
function validateEmail(email: string): boolean {
// 实际上只检查 @ 存在与否,远不如名字暗示的严格
}
Review 影响:
- 不能光看函数名判断作用
- 必须读实现确认名实相符
- 命名不准的直接改
1.3 边界条件覆盖
人类:经验丰富的工程师会自然想到 null、空数组、负数、超大值。
AI:常常缺失边界处理。它"知道"应该处理,但忘了具体处理。
// AI 经常这样
function getFirstItem<T>(arr: T[]): T {
return arr[0]; // 如果空数组返回 undefined,但类型说是 T
}
// 应该
function getFirstItem<T>(arr: T[]): T | undefined {
return arr[0];
}
Review 影响:
- 重点检查所有"输入域"是否被覆盖
- 类型签名是否反映了真实返回值
1.4 错误处理的形式主义
人类:会想"这里失败会怎样",针对性处理。
AI:经常加 try/catch 但 catch 里什么都不做,或者只是 console.log。
// AI 经常这样
try {
const data = await fetchUserProfile(id);
return data;
} catch (error) {
console.error(error); // 然后呢?
return null; // 调用方完全不知道发生了什么
}
// 应该
try {
return await fetchUserProfile(id);
} catch (error) {
if (error instanceof NotFoundError) {
throw new UserNotFoundError(id);
}
if (error instanceof NetworkError) {
return await fetchUserProfile(id); // 重试一次
}
logger.error('Unexpected error fetching profile', { id, error });
throw error; // 不认识的错误向上抛
}
Review 影响:
- 每个 try/catch 必须问"catch 里在干什么"
- "console.log 然后 return null" 是几乎肯定的 bug
1.5 抽象的过度或不足
人类:根据具体场景判断抽象层次。
AI:要么把 5 行代码拆成 3 个函数(过度),要么把 200 行塞一个函数(不足)。
过度抽象的特征:
BaseAbstractFactoryProvider类型的命名- 一个函数被一个文件调用
- 接口只有一个实现
不足抽象的特征:
- 200+ 行的函数
- 大量重复的逻辑块
- 嵌套 4 层以上的 if
Review 影响:
- 主动寻找抽象层次问题
- 不要被"看起来很专业"迷惑
1.6 测试的"纸面通过"
人类:写测试时通常想"这测试能不能真的发现 bug"。
AI:经常写"能跑通的测试",但实际上只覆盖 happy path,或者测试在测自己。
// AI 经常这样(看起来覆盖了)
it('should validate email', () => {
expect(validateEmail('foo@bar.com')).toBe(true);
});
// 缺失:'foo@bar' (无 .com), 'foo' (无 @), '' (空), null
更糟糕的:
// AI 在 mock 里把答案"写死"
it('should fetch user', async () => {
vi.mocked(api.getUser).mockResolvedValue({ id: '123', name: 'Alice' });
const result = await getUser('123');
expect(result).toEqual({ id: '123', name: 'Alice' });
// 这个测试只是测试"mock 返回什么就是什么",没测真实逻辑
});
Review 影响:
- 测试覆盖率高 ≠ 质量高
- 看每个测试是否有真正的断言能力
- mock 是不是过度,导致测试失去意义
1.7 上下文断裂
人类:写代码时"记得"前面写了什么,整个文件有连贯逻辑。
AI:context window 有限,长文件后半部分可能"忘了"前半部分的约定。
典型表现:
- 同一个文件里,错误处理风格不一致
- 同一个 class 里,方法签名风格不一致
- 跨文件不一致更严重
Review 影响:
- 必须全文扫描,不能局部审视
- 跨文件的约定一致性更要警惕
第二章:AI 代码的 12 类常见隐患
按发生频率和危险等级排序。
2.1 P0:编造的 import / API
症状:AI 引入了不存在的库、不存在的函数、不存在的字段。
import { advancedRetry } from 'axios'; // axios 没有这个导出
import { LRUCache } from 'lodash'; // lodash 没有
const result = supabase
.from('users')
.selectWithCount(); // 不存在的方法
为什么 AI 会这样:训练数据里见过类似的库 API,混淆或编造合理的方法名。
检测:
- TypeScript 编译会报错(必须
tsc --noEmit干净) - 运行时才暴露的:JS 弱类型、动态 import
- 必须实际跑测试或运行,不能只看 diff
2.2 P0:硬编码的密钥/敏感信息
// AI 看到 .env.example 里的占位符,可能直接复制
const apiKey = "sk-1234567890abcdef"; // 这可能是真的 key
const dbUrl = "postgres://user:realPassword@host/db";
检测:
- pre-commit hook 扫描密钥
- AI 改完后必看
git diff中的 .env / config 文件
2.3 P0:无声吞掉的错误
try {
await criticalOperation();
} catch (e) {} // 完全静默
try {
await fetchData();
} catch (e) {
console.log(e); // 生产环境看不到
}
检测:
- grep 项目里所有空 catch block
- grep
console.login catch
2.4 P1:类型欺骗
const user = data as User; // 强制断言,没验证
const id: string = response.id!; // ! 断言,假设非空
function getValue(): unknown {
return data as any; // 用 any 绕过类型
}
检测:
- grep
as any、as unknown as、@ts-ignore、@ts-expect-error - 每个都问"为什么需要绕过类型"
2.5 P1:异步竞态条件
// AI 经常忘记 await
function saveUser(user: User) {
db.save(user); // 没 await,可能没保存就返回了
return { success: true };
}
// 或者忘记并发安全
async function increment() {
const value = await getValue();
await setValue(value + 1);
// 两个并发调用 → 丢更新
}
检测:
- ESLint
@typescript-eslint/no-floating-promises - 心里过一遍"这里会不会并发问题"
2.6 P1:未处理的边界值
function calculateAverage(numbers: number[]): number {
return numbers.reduce((a, b) => a + b) / numbers.length;
// 空数组:reduce 抛错;1 个元素:忘了初始值
// 空数组:除以 0 = NaN
}
检测:
- 每个函数想三个 case:空、单元素、极端值
- 边界测试覆盖
2.7 P2:性能反模式
// AI 喜欢嵌套循环
for (const user of users) {
const orders = await getOrders(user.id); // N+1 查询
// ...
}
// AI 不知道你的数据量
const sorted = data.sort(...); // 100万条数据原地排序
检测:
- 看循环里的 await
- 看大数组操作
- 必要时上 profiler
2.8 P2:依赖污染
// AI 引入了不必要的大依赖
"dependencies": {
"lodash": "^4.17.21", // 你只用了 isEmpty
"moment": "^2.29.4" // 已废弃,应该用 dayjs
}
检测:
- 每次
package.json改动重点 review pnpm dlx depcheck找未用依赖
2.9 P2:测试质量低
详见 1.6 节。
2.10 P3:注释过度或不足
AI 倾向写过度的注释:
// 增加用户
function addUser(user: User) {
// 把用户加进来
users.push(user);
}
或者跨文件解释 TypeScript 类型:
// 这个是字符串
const name: string;
检测:
- 删掉无意义的注释
- 保留"为什么"的注释
2.11 P3:风格漂移
详见 1.7 节。
2.12 P3:过时的最佳实践
AI 的训练数据有 cutoff。可能用:
- React class component(应该用 hooks)
- componentWillMount(已废弃)
- Promise.then 链(应该用 async/await)
- enum(应该用 as const)
检测:
- 项目级 ESLint 规则
- 团队 CLAUDE.md 明确规定"用什么风格"
第三章:两阶段 Review 流程
3.1 流程总览
核心原则:
- Stage 1 100% 自动化——人不应该看 lint/typecheck 错误
- Stage 2 集中精力在"机械检查不到的地方"
- 不要跳过 Stage 1 直接 Stage 2——浪费人脑
3.2 Stage 1:机械检查清单
必须全部自动化跑,且必须 0 错误才进 Stage 2:
# .github/workflows/ai-pr-check.yml
name: AI PR Mechanical Check
on:
pull_request:
types: [opened, synchronize]
jobs:
mechanical-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1. 类型安全
- name: Type check
run: pnpm typecheck
# 2. Lint
- name: Lint
run: pnpm lint -- --max-warnings 0
# 3. 测试
- name: Test
run: pnpm test
# 4. 构建
- name: Build
run: pnpm build
# 5. 密钥扫描
- name: Secret scan
uses: trufflesecurity/trufflehog@main
# 6. 依赖审计
- name: Audit
run: pnpm audit --audit-level moderate
# 7. 测试覆盖率
- name: Coverage
run: pnpm test:coverage -- --coverage.threshold=80
# 8. AI 代码标记检查
- name: Check AI markers
run: ./scripts/check-ai-markers.sh
# 检查:commit message 里是否标注 [AI-assisted] / [AI-generated]
3.3 Stage 2:人类深度审查清单
每个 AI PR 必查的 20 项:
命名与意图(1-3)
- 函数名实际反映了行为?
- 变量名暗示的含义和实际值一致?
- 文件/类名表达了它真的在做什么?
边界与异常(4-7)
- 所有输入的边界 case 被处理(null / 空 / 极端值)?
- try/catch 里 catch 不是空的、不是只有 console.log?
- 网络/IO 失败的处理合理?
- 异步并发场景没有 race condition?
类型与正确性(8-10)
- 没有
as any、@ts-ignore、!断言(除非有充分理由)? - 类型签名反映真实返回值(包括 undefined / null)?
- 泛型用得对,不是为了用而用?
安全(11-13)
- 没有硬编码密钥或敏感信息?
- 用户输入做了校验?
- 没有引入 SQL/XSS/SSRF 注入风险?
性能(14-16)
- 循环里没有 N+1 查询/调用?
- 大数据操作合理(不是 100 万条原地排序)?
- 没有不必要的重复计算?
测试(17-18)
- 测试有真实断言能力,不是 mock 后断言 mock?
- 边界 case 有测试?
一致性(19-20)
- 风格和项目其他地方一致?
- 没有引入废弃的库或 API?
3.4 Review 时间预算
参考标准(基于 PR 大小):
| PR 行数 | Stage 1 | Stage 2 | 总时间 |
|---|---|---|---|
| < 50 | 自动 | 5 分钟 | 5 min |
| 50-200 | 自动 | 15 分钟 | 15 min |
| 200-500 | 自动 | 30-45 分钟 | 45 min |
| > 500 | 自动 | 拒收 | — |
原则:超过 500 行的 AI PR 必须拆分,否则 review 不过来。这就是为什么 06 Agentic Engineering 强调"先规划再执行"——好的规划自然产出小 PR。
3.5 双人 review 的特殊价值
对 AI 代码,双人 review 比对人代码更重要:
- 一个人看了的"明显问题",另一个人能看到"不明显的问题"
- AI 错误模式有特定特征,多人能覆盖更多模式
- 高敏感模块(auth、payment、user data)必须双人
实施:在 PR template 里区分
## Review 要求
- [ ] 至少一名工程师 review
- [ ] 涉及 auth/payment/user-data 时需要双人 review
- [ ] AI-generated 代码 + 高敏感模块 = 强制双人
第四章:自动化 Review 工具的边界
4.1 主流工具
2026 年成熟的 AI Code Review 工具:
| 工具 | 厂商 | 特点 | 主要用途 |
|---|---|---|---|
| CodeRabbit | CodeRabbit Inc | GitHub PR 自动评论 | 通用 review |
| Greptile | Greptile | 大 codebase 上下文 review | 复杂改动 |
| Cursor BugBot | Cursor | IDE 内 inline 提示 | 写代码时即时 |
| GitHub Copilot Review | GitHub | 集成 GitHub PR | 通用 |
| Bito | Bito | 多模型支持 | 团队定制 |
4.2 这些工具能做什么
强项:
- 找出明显的 bug 模式(null 检查、未处理 promise)
- 发现风格不一致
- 标记 TODO 和潜在问题
- 解释复杂代码段
- 建议命名改进
弱项:
- 业务逻辑正确性:它不知道你的业务规则
- 架构层面问题:它只看本 PR 的 diff
- 性能影响:它没有 profiler 数据
- 安全的深度问题:可能漏掉复杂注入
- 测试质量判断:它不知道哪些边界你真的需要
4.3 如何评估自动化工具
试用一周,看:
信号 1:误报率
- 高误报(> 30%)→ 工程师会忽略,等于没用
- 低误报(< 10%)→ 工程师会认真看
信号 2:真阳性
- 它发现的问题,你 review 时也会发现 → 价值低
- 它发现的问题,你可能漏掉 → 价值高
信号 3:响应速度
- PR 提交后 5 分钟内有评论 → 集成进 flow
- > 30 分钟 → 你已经 review 过了,没意义
信号 4:上下文质量
- 它看到的只是 diff → 评论可能不准
- 它能看到全 codebase → 评论更准(Greptile 的优势)
4.4 工具 + 人的最佳组合
工具的任务:
- Stage 1 的扩展(更广的机械检查)
- 一些常见模式(null 检查、性能反模式)
- 给人 review 提示
人的任务:
- 业务逻辑正确性
- 架构合理性
- 测试质量
- 命名意图
- 边界覆盖
不要:
- 让工具替代 Stage 2 人类 review
- 让人重复做工具能做的检查
4.5 自建团队级 Review Bot
很多大团队选择自建:
// 简化思路:基于 LLM 的自定义 Review Bot
async function reviewPR(pr: PullRequest) {
// 1. 拉取 diff + 相关上下文
const diff = await github.getPRDiff(pr);
const relatedFiles = await findRelatedFiles(diff);
// 2. 用 LLM + 团队规范做 review
const review = await llm.review({
diff,
context: relatedFiles,
teamRules: await loadCLAUDEmd(),
historicalPatterns: await loadCommonIssues(),
});
// 3. 过滤误报
const filtered = await filterFalsePositives(review);
// 4. 发到 PR
await github.postReviewComments(pr, filtered);
}
为什么自建:
- 用团队的 CLAUDE.md 做 review 标准
- 学习历史 issue 的常见错误模式
- 不发送代码到第三方(合规)
第五章:团队级 AI Code Review 规范
5.1 PR 模板
## 改动说明
## AI 使用声明
- [ ] 本 PR 包含 AI 辅助生成的代码
- 使用工具:☐ Claude Code ☐ Cursor ☐ Copilot ☐ Codex
- AI 辅助范围:☐ 整体生成 ☐ 局部辅助 ☐ 仅自动补全
- 我已经:☐ 完整读过每一行代码 ☐ 理解每个改动的目的
## 自测清单
- [ ] pnpm typecheck 通过
- [ ] pnpm lint 通过
- [ ] pnpm test 通过
- [ ] 手动测试核心流程
- [ ] 检查无密钥/敏感信息提交
- [ ] 边界 case 已考虑
## Review 重点
说明本 PR 中你最希望 reviewer 关注的部分(特别是 AI 生成的代码)
5.2 Commit Message 规范
区分 AI 参与程度:
# 完全人写
feat(cart): add quantity validator
# AI 辅助(你主导,AI 帮忙)
feat(cart): add quantity validator [AI-assisted]
# AI 主导(AI 写大部分,你 review)
feat(cart): add quantity validator [AI-generated]
# Co-author 标签(GitHub 风格)
feat(cart): add quantity validator
Co-authored-by: Claude <noreply@anthropic.com>
5.3 Review 责任划分
作者(Author):
- 我对这段代码完全负责
- 我能解释每一行的含义
- 我做过自测
Reviewer:
- 我对合并到 main 负责
- 发现问题主动 block
- 不理解的地方主动问
不可以:
- "AI 写的我没看,你看吧"
- "看起来对就 LGTM"
- "测试过了就行"
5.4 培训计划
新成员加入团队(或刚开始用 AI)的培训:
第 1 周:禁止用 AI 提交代码
- 学项目结构、规范
- 体验"完全自己写"的感觉
第 2-4 周:只用 AI 补全(Copilot 风格)
- 不能用 Composer / Agent
- 学会即时审视 AI 建议
第 2 月:可以用 Coding Agent,但
- 每个 PR 必须双人 review
- 必须能完整解释每一行
- 接受 review 严格反馈
第 3 月+:完整工作流
- 可以并行 / 异步 Agent
- review 标准和资深一致
第六章:高频反模式
6.1 "看起来 LGTM"
症状:AI 代码看起来很专业、命名规范、有注释,reviewer 扫一眼就 LGTM。
真实:所有的"看起来"都是 AI 训练目标。它就是要看起来对。
对策:
- 强制 review 时间下限(比如 PR 行数 / 30 分钟)
- 强制 reviewer 写实质性评论(不只是 "LGTM")
- 抽查制度:随机抽 10% PR 三人复审
6.2 "AI 又改了 review 提的问题"
症状:你 comment 一个问题,AI 修了,但只修了你 comment 的那个点,类似问题在其他地方还在。
对策:
- comment 时明确说"这类问题在文件里其他地方也有,全部修"
- review 时主动搜索同类问题
- 在 CLAUDE.md 写"修问题时检查同类"
6.3 "我看不懂这段,但测试通过"
症状:reviewer 不理解某段代码,但跑测试通过就批准。
真实:你不理解 = 你不能维护。今天 LGTM,6 个月后这块出 bug 你完全不知道怎么改。
对策:
- 看不懂 = 让作者重写
- 看不懂 = 加详细注释
- 不允许"测试通过 = 合格"
6.4 "AI 时代不需要 review"
症状:团队里有人主张"AI 写的足够好,节省 review 时间"。
对策:摆数据。让看看过去 1 个月 AI PR 引入了多少 bug。基本上数据立刻打脸。
第七章:度量 review 质量
7.1 关键指标
| 指标 | 目标 | 说明 |
|---|---|---|
| PR review 平均时间 | 与 PR 大小成比例 | 跳过严重 = 危险信号 |
| Review 评论数(实质性) | > 0 | 每个 PR 至少有 1 条实质评论 |
| 上线后 bug 归因率 | 下降 | review 应该挡住大部分 bug |
| AI 代码 vs 人代码的 bug 率 | 接近 | AI 代码 bug 显著高 = review 不够 |
7.2 抽查机制
每月:
- 随机抽 10% 已合并 AI PR
- 三人 review 重新审查
- 发现 review 漏掉的问题 → 团队学习
每季度:
- 看上线 bug 中有多少是"PR review 该挡住的"
- 调整 review 标准
7.3 文化指标
| 现象 | 健康度 |
|---|---|
| Reviewer 经常发现 AI 代码问题 | ✅ 健康 |
| Reviewer "LGTM" 很多 | ⚠️ 警惕 |
| 作者经常因为 review 重写 | ✅ 健康 |
| 作者抱怨 review 严格 | ✅ 通常健康(除非过度) |
| 大家不愿意 review | ❌ 不健康 |
第八章:实战例子
8.1 例:一段"看起来 OK"的 AI 代码
// AI 生成
export async function syncUserData(userId: string): Promise<UserData | null> {
try {
const remote = await fetchRemoteUser(userId);
const local = await db.users.findById(userId);
if (!remote) return local;
if (!local) {
await db.users.create(remote);
return remote;
}
if (remote.updatedAt > local.updatedAt) {
await db.users.update(userId, remote);
return remote;
}
return local;
} catch (error) {
console.error('Sync failed', error);
return null;
}
}
初看 LGTM?再仔细看:
- 错误处理:
return null让调用方完全不知道是网络问题、找不到用户、还是其他。 - 竞态:远端 fetch 和本地查同时进行,但之后的 update 是基于"刚才看到的"local。如果两边并发更新,会丢更新。
- 时间比较:
remote.updatedAt > local.updatedAt—— 如果两个系统时钟不同步呢? - 类型欺骗:返回
UserData | null,但null含义是"失败",调用方很难处理。 - 测试:这段需要测:远端无、本地无、双方都有但远端新、双方都有但本地新、网络失败、并发情况。AI 大概率只测了 happy path。
每一条都是 review 时该提出的。
8.2 例:依赖升级 PR
AI 升级了 30 个 npm 包。Diff 主要在 package.json + lock 文件,看起来"机械"。
陷阱:
- 某个包从 4.x 到 5.x 是 major 版本——AI 可能没注意 breaking change
- 某个包名改了(
react-spring→@react-spring/web),AI 可能没全部跟进 - 测试通过 ≠ 没问题——很多 breaking change 在 runtime 才暴露
Review 必查:
- 看 changelog(不是 release notes,看真实 CHANGELOG.md)
- 跑 e2e 测试(不只单元测试)
- 部署 preview,手动测核心流程
8.3 例:AI 修复"flaky test"
PR 描述:"fix flaky test"。
// 之前
expect(result).toBe(expected);
// AI 改成
expect(result).toBeDefined(); // 弱化断言
这是地震级红旗:AI 把测试改弱,不是修问题。
Review 必须:
- 任何"测试改动"PR 单独严格审
- 弱化断言 = 几乎肯定 reject
- 实在不能精确断言,要解释为什么
第九章:未来方向
9.1 AI Review AI
让一个 LLM review 另一个 LLM 写的代码?理论可行,2026 年开始流行:
- 优点:批量、便宜、覆盖广
- 缺点:同样的盲区、同样的偏好、可能漏共同模式
最佳实践:用作"机械检查的扩展",不替代人类。
9.2 模型间交叉 review
让 Claude 写代码 + GPT review。不同模型有不同偏好,能捕捉到对方漏掉的问题。
实战:
Claude Code 写代码 → push PR
↓
CodeRabbit (基于 GPT) 自动 review
↓
人类看双方意见后判断
9.3 静态分析 + LLM 结合
传统静态分析(Semgrep、Sonar)+ LLM 解读:
- 静态分析提供精确警告
- LLM 解释含义、给修复建议
- 结合两者优点
9.4 PR 拆分 Bot
AI Agent 主动拆分大 PR:
- 检测 PR > 500 行
- 自动按文件 / 关注点拆成多个小 PR
- 改善 review 效率
权威资料
- Vibe engineering / Agentic Engineering (Simon Willison, 2025-10)
- Hallucinations in code are the least dangerous form of LLM mistakes (Simon Willison, 2025-03)
- Your job is to deliver code you have proven to work (Simon Willison, 2025-12)
- CodeRabbit 文档
- Greptile 文档
- 06 Agentic Engineering(前置)
- 05 Parallel & Async Coding Agents
- 04 Lethal Trifecta
核对日期:2026-06-12